package com.yh.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yh.anno.IdempotencyHandler;
import com.yh.utils.Constants;
import com.yh.vo.ResponseVO;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import java.util.concurrent.TimeUnit;

/**
 * 幂等性拦截器 防止短时间内订单出现重复支付
 * 1 以orderID是否在redis中存在为依据，判断订单是不是被重复支付
 * 2 如果获取操作权限时出现异常，拦截器将在设定的等待时间内持续获取锁，直到等待时间结束
 *
 * @author yuhuan
 * @date 2022/01/13
 */
public class IdempotencyInterceptor implements HandlerInterceptor {
    /**
     * 日志
     */
    private final Logger logger = Logger.getLogger(IdempotencyInterceptor.class);

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (IsInterceptorToPreHandle.isInterceptor(handler, IdempotencyHandler.class)) {
            // 获取订单ID
            String orderId = request.getHeader("orderId");
            orderId = URLDecoder.decode(orderId, "UTF-8");
            if (lock(orderId, Constants.OrderData.IDEMPOTENCY_WAIT_TIME, Constants.OrderData.IDEMPOTENCY_EXPIRE_TIME)) {
                return true;
            }
            // 响应支付失败
            logger.warn(">>>>> 订单正在处理中，请勿重复操作");
            ResponseVO responseVO = ResponseVO.buildSuccessInstance(Constants.ResponseDataState.PAY_FAIL_OF_REPEAT);
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(responseVO));
            return false;
        }
        return true;
    }

    /**
     * 判断该 ID 在 redis 中是否已经存在，如果不存在就获取操作权限
     */
    private Boolean isLocking(String orderId, Long expireTime, TimeUnit unit) throws RuntimeException {
        // 防空，权限获取失败
        if (ObjectUtils.isEmpty(orderId)) {
            return false;
        }
        // 将 orderId 添加到redis中，添加成功即表示获取操作权限成功
        return redisTemplate.opsForValue().setIfAbsent(orderId, orderId, expireTime, unit);
    }

    /**
     * 如果获取幂等性操作权限成功！就直接返回；如果失败，就在等待的时间内持续获取权限，如果最终都没有获取到权限就获取失败
     */
    private Boolean lock(String orderId, Long waitTime, Long expireTime) {
        try {
            // 获取操作权限
            return isLocking(orderId, expireTime, TimeUnit.SECONDS);
        } catch (RuntimeException re) {
            // 获取现在的时间(毫秒级)
            long millis = System.currentTimeMillis();
            // 计算等待的结束时间(毫秒级)
            long waitTimeEnd = millis + waitTime * 1000;
            // 在等待的时间内不断尝试获取锁
            while (System.currentTimeMillis() < waitTimeEnd) {
                boolean locking = isLocking(orderId, expireTime, TimeUnit.SECONDS);
                if (locking) {
                    return locking;
                }
                try {
                    logger.error(">>>>> 获取幂等性锁失败！剩余等待时间" + (waitTimeEnd - System.currentTimeMillis()) / 1000 + "秒");
                    // 休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException ie) {
                    return false;
                }
            }
            logger.error("<<<<< 幂等性验证请求超时");
        }
        return false;
    }
}
